spb/tools-spboucher-ai
Public
1import { z } from "zod";2import { eq } from "drizzle-orm";3import { db, schema, sql } from "@/db";4import { requireUser, jsonError, HttpError } from "@/lib/auth/guard";5import { USE_STATES } from "@/lib/content/schema";6import { audit } from "@/lib/audit";7const body = z.object({ use_state: z.enum(USE_STATES), rating: z.number().int().min(1).max(5).nullable().optional(), notes: z.string().max(8000).nullable().optional(), cluster_compat: z.enum(["yes", "partial", "no", "unknown"]).default("unknown"), priority: z.enum(["high", "medium", "low"]).default("medium"), subscription_status: z.string().max(120).nullable().optional(), deployment_urls: z.array(z.object({ label: z.string().max(80), url: z.string().url().max(500) })).max(20).default([]), custom_tags: z.array(z.string().max(40)).max(20).default([]), review_at: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional(), projects: z.array(z.string()).default([]) });8type P = { params: Promise<{ toolId: string }> };9export async function GET(_: Request, { params }: P) {10 try { await requireUser(); const { toolId } = await params; const [n] = await db.select().from(schema.ownerNotes).where(eq(schema.ownerNotes.toolId, toolId)); const projects = await sql<{ slug: string }[]>`SELECT p.slug FROM project_tools pt JOIN projects p ON p.id=pt.project_id WHERE pt.tool_id=${toolId}`; return Response.json({ note: n ?? null, projects: projects.map((p) => p.slug) }); } catch (e) { return jsonError(e); }11}12export async function PUT(req: Request, { params }: P) {13 try {14 const s = await requireUser(req); if (s.role !== "owner") throw new HttpError(403, "Owner only"); const { toolId } = await params; const b = body.parse(await req.json());15 const [t] = await db.select({ id: schema.tools.id, slug: schema.tools.slug }).from(schema.tools).where(eq(schema.tools.id, toolId)); if (!t) throw new HttpError(404, "Tool not found");16 const vals = { toolId, useState: b.use_state, rating: b.rating ?? null, notes: b.notes ?? null, clusterCompat: b.cluster_compat, priority: b.priority, subscriptionStatus: b.subscription_status ?? null, deploymentUrls: b.deployment_urls, customTags: b.custom_tags, reviewAt: b.review_at ?? null, updatedAt: new Date() };17 await db.insert(schema.ownerNotes).values(vals).onConflictDoUpdate({ target: schema.ownerNotes.toolId, set: vals });18 await db.delete(schema.projectTools).where(eq(schema.projectTools.toolId, toolId));19 for (const ps of b.projects) { const [p] = await db.select({ id: schema.projects.id }).from(schema.projects).where(eq(schema.projects.slug, ps)); if (p) await db.insert(schema.projectTools).values({ projectId: p.id, toolId }).onConflictDoNothing(); }20 await audit({ id: s.id, name: s.email }, "owner.note_saved", { type: "tool", id: toolId }, { slug: t.slug, use_state: b.use_state });21 return Response.json({ ok: true });22 } catch (e) { return jsonError(e); }23}24